從今天開始到 Day 22 共 13 天
將會以 CryptoZombie 這個 solidity 學習網站
來了解 Solidity 這個語言
學完之後將能夠知道如何透過 Solidity 來開發 Small Contract
Solidity 語言是 EVM 上能夠運行的一種腳本語言,用來撰寫 Small Contract
在這 13 天的介紹中
將分為成以下大章節:
Smart Contract 基礎資料結構與 Web3.js 互動介面:Day 9 - Day 13
Smart Contract 與去中心化 Oracle 互動: Day 14
Smart Contract 測試與如何建立 Oracle: Day 15 - Day 19
Smart Contract 周邊系統: Day 20 - Day 21
可以開啟另一個視窗跟著本文一起去實作
課程連結如下:
在開始撰寫 Contract 之前
先理解要實作的需求
ZombieContract 是一個生成 Zombie 物件的工廠
每個 Zombie 具有 name 與 dna 屬性
並且具有一個識別子 id 作為一個辨識生成過的編號
name 是一個隨機字串
dna 是由 id 與 name 做雜湊產生出來的字串
根據這些參數每個 Zombie 物件有都是有他獨特的樣貌
在每個 Contract 的代碼前都必須要標注編譯的 Solidity 版本號
因為每個版本都有其特別的功能及不同的資料結構支援
舉例來說:
要撰寫一個由 solidity 版本於 0.5.0 及 0.6.0 之間的 Contract
必須在上方宣告如下:
pramga solidity >=0.5.0 <0.6.0;
Contract 本體如同物件導向語言裏面,對於物件會宣告一個建立物件的類別
當作建立物件的藍本。
Contract 也是一樣,會宣告一個開頭為 contract 帶有名稱的腳本區段
當作建立該 Contract 的腳本。
如下:
建立一個 ZombieFactory 的空腳本
pramga solidity >=0.5.0 <0.6.0;
contract ZombieFactory {}
在 solidity 裏面,當在 contract 內部宣告一個變數時
這個變數將會永遠寫入 constract 的儲存空間內。代表狀態變數會被寫入區塊鏈上。
舉例來說:
contract Example {
// myUnsignedInteger 會變成 Contract 內容寫入鏈上
uint myUnsignedInteger = 100;
}
在 solidity , uint 代表這個整數沒有負號,只能存正數。
另外,uint 代表 uint256 的別名。
其他 bit 位數的無號數還有 uint8, uint16, uint32 等等。
在 ZombieFactory 內,需要一個無號數來存儲 dnaDigits 並且設定為 16。
到此 ZombieFactory 如下
pramga solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
}
在 solidity , 數值運算與一般程式語言一樣:
另外也支援指數運算
uint x = 5**2; // x = 5*5 = 25
為了保證 Zombie 的 DNA 字元只有 16 個字元長
所以建立一個 uint 變數 dnaModulus = dnaDigits的16次方
這樣每次只要把數值 % dnaModulus 就會滿足這個條件
更新 ZombieFactory 如下
pramga solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = dnaDigits**16;
}
在 solidity 除了基礎的資料結構外,也支援複合型資料結構
透過 struct 來做宣告
舉例如下:
struct Person {
uint age;
string name;
}
其中 string 是儲存 UTF-8 編碼的字串,也就是每個字元只有 8-bit。
因為每個 Zombie 需要有屬於自己的資料,也就是 dna 跟 name
所以需要建立一個 struct 名稱設定為 Zombie
Zombie 具有兩個屬性:
更新 ZombieFactory 如下
pramga solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = dnaDigits**16;
struct Zombie {
string name;
uint dna;
}
}
當要存儲大量相同類型的資料時
就可以使用 array
在 Solidity ,根據陣列長度是否為固定分成 fixed 與 dynamic
// fixed array 資料集長度固定
uint[2] fixedArray;
// dynamic array 資料集長度不固定
uint[] dynamicArray;
狀態資料可以把其存取屬性設定為 public , solidity 會自動替這個屬性產生 getter,讓其他有用到的地方讀取。
語法如下
Person[] public people;
在 ZombieFactory ,需要使用一個 dynamic Array 來紀錄所有產生出來的 Zombie 物件,並且需要讓這個 dynamic Array 存取權限是 public。
更新 ZombieFactory 如下
pramga solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = dnaDigits**16;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
}
函式宣告語法如下
function eatHamburgers(string memory _name, uint _amount) public {}
函式宣告包含元素如下:
函式名稱: 如上面函式名稱是 eatHamburgers
參數: 參數根據其傳遞方式分為兩種。 pass by value:傳值,只會把值做複製傳入,在函式中修改不影響其原本的值。 pass by reference:傳參考,會把該參數的參考指標傳入,在函式中修改其值,會透過參考指標改到原本的值,對於參考值需要加入 memory 在參數前面。而參考類型比如 string, structs, mapping, 還有 arrays都需要。
存取權限:這邊的 eatHamburgers 是 public,代表可以直接從 abi 呼叫這個 function
特別注意的是,習慣上的命名規則會把傳入參數以底線當作開頭。
ZombieFactory 需要加入一個 public function 名稱叫作 createZombie。 createZombie 需要兩個參數一個是字串 _name,一個是 uint _dna
更新 ZombieFactory 如下
pramga solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = dnaDigits**16;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function createZombie(string memory _name, uint _dna) public {
}
}
在 solidity 中,
產生 struct 的語法如下:
struct Person {
uint age;
string name;
}
// 產生一個新的 Person
Person satoshi = Person(20, "Satoshi");
array 可以透過 array.push 新增元素到 array 之內
舉例如下
Person[] public people;
people.push(Person(20, "Satoshi"));
createZombie 會產生一個新的 Zombie,並且把這個 Zombie 加入 zombies array。
更新 ZombieFactory 如下
pramga solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = dnaDigits**16;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function createZombie(string memory _name, uint _dna) public {
zombies.push(Zombie(_name, _dna));
}
}
solidity 對於 function, 有 public 與 private 權限。
在 solidity , function 預設都是 public。代表所有人都可以透過 abi 來呼叫 function 。
然而這樣會很容易把一些能夠修改資料的邏輯給透露給所有人,並非是一種安全的設計。
因此比較好的方式,把 function 預設設定為 private 限定只有 contract 物件可以呼叫。
只把一些需要外部互動的部份給設定成 public。
如下:
uint[] numbers;
function _addToArray(uint _number) private {
numbers.push(_number);
}
通常習慣把 private 函式命名以底現作為開頭。
更新 ZombieFactory 如下
pramga solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = dnaDigits**16;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function _createZombie(string memory _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
}
讓 function 具有回傳值的語法如下:
string greeting = "What's up dog";
function sayHello() public returns (string memory) {
return greeting;
}
如下:
string greeting = "What's up dog";
function sayHello() public view returns (string memory) {
return greeting;
}
如下:
function _multiply(uint _a, uint _b) private pure returns (uint) {
return _a * _b;
}
每個 Zombie 具有一個特徵值 dna
ZombieFactory 會需要一個產生隨機數的 function 來處理這個功能。
建立一個 private function 名稱設定為 _generateRandomDna。_generateRandomDna 具有一個字串參數 _str,並且具有一個 uint 回傳值。
_generateRandomDna 不能修改 Contract 其他值,需要加入 view 修飾子。
更新 ZombieFactory 如下
pramga solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = dnaDigits**16;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function _createZombie(string memory _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
function _generateRandomDna(string memory _str) private view returns(uint) {
}
}
Ethereum 內建 keccak256 函式是一個用來做雜湊值的函數,把輸入資料對應到 256-bit 的 16進位數值。
輸入必須是 bytes,所以需要把輸入字串透過 abi.encodedPacked轉換成 byte。如下:
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
keccak256(abi.encodePacked("aaaab"));
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
keccak256(abi.encodePacked("aaaac"));
型別轉換是用來轉換兩種不同型別,語法如下
uint8 a = 5;
uint b = 6;
// throws an error because a * b returns a uint, not uint8:
uint8 c = a * b;
// we have to typecast b as a uint8 to make it work:
uint8 c = a * uint8(b);
更新 ZombieFactory 如下
pramga solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = dnaDigits**16;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function _createZombie(string memory _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
function _generateRandomDna(string memory _str) private view returns(uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
}
更新 ZombieFactory 如下
pramga solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = dnaDigits**16;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function _createZombie(string memory _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
function _generateRandomDna(string memory _str) private view returns(uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string memory _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randomDna);
}
}
Events 是一種讓 Contract 來根據 blockchain 資料變動來與前端應用互動的方式,透過這種方式可以監聽一些特定的變動。
如下:
// declare the event
event IntegersAdded(uint x, uint y, uint result);
function add(uint _x, uint _y) public returns (uint) {
uint result = _x + _y;
// fire an event to let the app know the function was called:
emit IntegersAdded(_x, _y, result);
return result;
}
前端應用就可以透過以下語法來監聽:
YourContract.IntegersAdded(function(error, result) {
// do something with result
})
更新 ZombieFactory 如下
pramga solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = dnaDigits**16;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function _createZombie(string memory _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
emit NewZombie(id, _name, _dna)
}
function _generateRandomDna(string memory _str) private view returns(uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string memory _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randomDna);
}
}
到這裡,完整的 ZombieFactory 邏輯完成了
當把整個 ZombieFactory Contract Deploy 到已太鏈上時,
我們可以透過產生出來的 abi 使用 web3.js 從前端去呼叫 contract 來產生隨機 Zombie。